/**
 * 
 */
package fr.utbm.gi.in56.database;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Date;
import java.util.Vector;
import java.util.Properties;

/**
 * Implementation of a connection pool for database connections.
 *
 * @author Adrien Hoffer & Tarik Belabbas
 *
 */
public class ConnectionPool
{
	/**
	 * The JDBC driver name.
	 */
	private String						jdbcDriverName;
	
	/**
	 * The JDBC database connection URL. 
	 */
	private String						jdbcConnectionURL;
	
	/**
	 * The pool size.
	 */
	private int							connectionPoolSize;
	
	/**
	 * The pool max connections. 
	 */
	private int							connectionPoolMax;
	
	/**
	 * The pool use counter.
	 */
	private int							connectionUseCount;
	
	/**
	 * The database user.
	 */
	private String						dbUser;
	
	/**
	 * The database password.
	 */
	private String						dbPassword;
	
	/**
	 * The connection pool.
	 */
	private Vector< ConnectionObject >	pool;

	/**
	 * Default constructor.
	 */
	public ConnectionPool()
	{

	}

	/**
	 * Initializes the connection pool
	 */
	public void initialize()
	{
		String config = "ConnectionPool.cfg"; //$NON-NLS-1$
		
		boolean loaded = loadConfig( config );
		if ( !loaded )
		{
			System.out.println( "Error loading config file." ); //$NON-NLS-1$
		}
		else
		{
			this.pool = new Vector< ConnectionObject >();
			fillPool( this.connectionPoolSize );
		}
	}

	/**
	 * Reads the the configuration file and store its data.
	 * 
	 * @param config the name of the configuration file
	 * @return <tt>true</tt> if the configuration file is successfully loaded
	 */
	private boolean loadConfig( String config )
	{
		InputStream inputStream = null;
		boolean isLoaded = false;
		try
		{
			// Get the loader for this class
			ClassLoader loader = getClass().getClassLoader();
			
			if ( loader != null )
			{
				inputStream = loader.getResourceAsStream( config );
			}
			else
			{
				inputStream = ClassLoader.getSystemResourceAsStream( config );
			}

			// If the input stream is null, the configuration file was not found
			if ( inputStream == null )
			{
				System.out.println( "ConnectionPool configuration file, '" //$NON-NLS-1$
						+ config + "', not found" ); //$NON-NLS-1$
			}
			else
			{
				Properties jdbcProperties = new Properties();
				
				// Load the configuration file into the properties table
				jdbcProperties.load( inputStream );

				this.jdbcDriverName		= jdbcProperties.getProperty( "JDBCDriver" ); //$NON-NLS-1$
				this.jdbcConnectionURL	= jdbcProperties.getProperty( "JDBCConnectionURL" ); //$NON-NLS-1$
				this.connectionPoolSize	= Integer.parseInt(
						jdbcProperties.getProperty( "ConnectionPoolSize" ) ); //$NON-NLS-1$
				this.connectionPoolMax	= Integer.parseInt(
						jdbcProperties.getProperty( "ConnectionPoolMax" ) ); //$NON-NLS-1$
				this.connectionUseCount	= Integer.parseInt(
						jdbcProperties.getProperty( "ConnectionUseCount" ) ); //$NON-NLS-1$
				this.dbUser				= jdbcProperties.getProperty( "DbUser" ); //$NON-NLS-1$
				this.dbPassword			= jdbcProperties.getProperty( "DbPassword" ); //$NON-NLS-1$
				
				isLoaded = true;
			}
		}
		catch ( IOException _exception )
		{
			System.out.println( "IOException" ); //$NON-NLS-1$
			_exception.printStackTrace();
		}
		finally
		{
			// Close the input stream.
			if ( inputStream != null )
			{
				try
				{
					inputStream.close();
				}
				catch ( IOException ex )
				{
				}
			}
		}
		
		return isLoaded;
	}

	/**
	 * Gets a connection and then .  
	 * It then 
	 * 
	 * @param poolSize
	 */
	private synchronized void fillPool( int poolSize )
	{
		try
		{
			Connection con;
			int count = 0;
			int maxConnections = 0;
			while ( count < poolSize )
			{
				ConnectionObject conObject = new ConnectionObject();
				Class.forName( this.jdbcDriverName );
				con = DriverManager.getConnection( this.jdbcConnectionURL, this.dbUser, this.dbPassword );
				conObject.setConnection( con );
				
				// First time through the loop, find out the maximum number of
				// connections that the database can support
				if ( count == 0 )
				{
					con = conObject.getConnection();
					DatabaseMetaData metaData = con.getMetaData();
					
					// Check to see how many connections the database can support
					maxConnections = metaData.getMaxConnections();
					
					// If it is less than poolSize and not 0, the pool size is adjusted
					if ( ( maxConnections != 0 ) && ( poolSize > maxConnections ) )
					{
						System.out.println( "Pool size too large." ); //$NON-NLS-1$
						poolSize = maxConnections;
					}
				}
				
				conObject.setInUse( false );
				conObject.setUseCount( 0 );
				conObject.setLastAccessTime( new Date() );
				
				// Fill the vector with new ConnectionObjects
				this.pool.addElement( conObject );
				count++;
			}
		}
		catch ( SQLException e )
		{
			System.out.println( "SQLException" ); //$NON-NLS-1$
		}
		catch ( ClassNotFoundException e )
		{
			System.out.println( "Class Not Found exception.\n" ); //$NON-NLS-1$
		}
	}
 
     /**
      * Returns the connection. Synchronized so that only one servlet can get a
      * connection at a time.
      * 
     * @return the connection
     */
	public synchronized Connection getConnection()
	{
		Connection connection = null;
		ConnectionObject connectionObject = null;
		ConnectionObject conObject = null;
		int poolSize = pool.size();
		int count = 0;
		boolean found = false;

		if ( pool == null )
		{
			return null;
		}
		
		// Find the first available connection in the pool.
		while ( ( count < poolSize ) && !found )
		{
			conObject = ( ConnectionObject ) pool.elementAt( count );
			if ( conObject.isAvailable() )
			{
				found = true;
			}
			else
			{
				count++;
			}
		}
		
		if ( found )
		{
			connectionObject = conObject;
		}

		if ( connectionObject == null )
		{
			System.out.println( "All connections in use." ); //$NON-NLS-1$
		}
		else
		{
			connectionObject.setInUse( true );
			connectionObject.setUseCount( connectionObject.getUseCount() + 1 );
			connectionObject.setLastAccessTime( new Date() );
			
			connection = connectionObject.getConnection();
		}
		
		return connection;
	}

	/**
	 * Close a connection, i.e., marks it as unused and makes it available again.
	 * 
	 * @param _connection the connection to close
	 */
	public synchronized void close( Connection _connection )
	{
		// Find the connection in the pool
		int index = find( _connection );
		
		if ( index != -1 )
		{
			ConnectionObject conObject = ( ConnectionObject ) pool.elementAt( index );
			
			conObject.setInUse( false );
			conObject.setLastAccessTime( new Date() );
		}
		else
		{
			System.out.println( "Connection not found in pool." ); //$NON-NLS-1$
		}
	}
	
	/**
	 * Searches through the vector a connection and returns its index.
	 * 
	 * @param _connection the connection to find
	 * @return the index of the connection if it is found, -1 otherwise 
	 */
	private int find( Connection _connection )
	{
		int index = 0;
		boolean found = false;
		try
		{
			ConnectionObject conObject;
			String catalog = _connection.getCatalog();

			while ( ( index < pool.size() ) && !found )
			{
				conObject = ( ConnectionObject ) pool.elementAt( index );
				Connection poolCon = conObject.getConnection();
				
				String name = poolCon.getCatalog();
				if ( catalog.equals( name ) )
				{
					found = true;
				}
				else
				{
					index++;
				}
			}
		}
		catch ( SQLException e )
		{
			System.out.println( "Catalog Exception" ); //$NON-NLS-1$
		}
		
		if ( found )
		{
			return index;
		}
		else
		{
			return -1;
		}
	}

	/**
	 * Closes each connection in the pool and then sets the pool to null. 
	 */
	public void destroy()
	{
		try
		{
			if ( pool != null )
			{
				// Loop the connection objects vector
				for ( int count = 0; count < pool.size(); count++ )
				{
					ConnectionObject connectionObject =
						( ConnectionObject )pool.elementAt( count );
					Connection connection = connectionObject.getConnection();
					
					// Close the connection
					connection.close();
				}
			}
		}
		catch ( SQLException e )
		{
			System.out.println( "Destroy error." ); //$NON-NLS-1$
		}
		
		pool = null;
	}
}